Skip to content

feat: devnet build configuration#180

Merged
WiktorStarczewski merged 2 commits intomainfrom
wiktor-devnet-setup
Apr 10, 2026
Merged

feat: devnet build configuration#180
WiktorStarczewski merged 2 commits intomainfrom
wiktor-devnet-setup

Conversation

@WiktorStarczewski
Copy link
Copy Markdown
Collaborator

Summary

  • Adds MIDEN_NETWORK env variable to control build-time network targeting (default: testnet)
  • yarn build:devnet produces a devnet extension with blue/gray branding, devnet endpoints, and "Miden Wallet (Devnet)" manifest name
  • Replaces all hardcoded brand color hex values (#FF5500, #EE622F, etc.) with network-conditional constants from src/utils/brand-colors.ts
  • Adds 14 devnet SVG icon variants and conditional icon selection in Settings, Onboarding, Unlock, and BiometricUnlock screens

New build scripts

  • yarn build:devnet — extension
  • yarn dev:devnet — extension dev watch
  • yarn build:mobile:devnet — mobile
  • yarn build:desktop:devnet — desktop

How it works

One env variable (MIDEN_NETWORK) drives everything:

  • Webpack EnvironmentPlugin injects it into all 7 build configs
  • constants.ts reads it to set DEFAULT_NETWORK → controls endpoints
  • tailwind.config.ts reads process.env.MIDEN_NETWORK → controls CSS colors
  • brand-colors.ts reads DEFAULT_NETWORK → controls inline/SVG colors
  • webpack.public.config.js transform → controls manifest name

A plain yarn build (no env var) is unchanged — testnet by default.

Test plan

  • yarn build produces testnet extension (orange, "Miden Wallet")
  • yarn build:devnet produces devnet extension (blue/gray, "Miden Wallet (Devnet)")
  • yarn test — 1639 tests passing
  • yarn lint — clean (0 warnings)
  • Load both extensions side-by-side and verify visual difference

Build-time configurable devnet support: set MIDEN_NETWORK=devnet to
produce a devnet wallet with blue/gray branding, devnet endpoints,
and "(Devnet)" manifest name. Testnet remains the default.

New scripts: build:devnet, dev:devnet, build:mobile:devnet, build:desktop:devnet
@WiktorStarczewski WiktorStarczewski added the no changelog Skip changelog check for trivial changes label Apr 10, 2026
@WiktorStarczewski WiktorStarczewski merged commit b45c6a2 into main Apr 10, 2026
10 checks passed
@WiktorStarczewski WiktorStarczewski deleted the wiktor-devnet-setup branch April 10, 2026 11:29
WiktorStarczewski added a commit that referenced this pull request Apr 14, 2026
…rker path

Two regressions in the E2E blockchain test pipeline, both surfaced when
switching to devnet:

1. test:e2e:blockchain:build was hardcoded to testnet. MIDEN_NETWORK was
   never propagated to the extension build, so with E2E_NETWORK=devnet
   the test harness deployed faucets on devnet while the wallet quietly
   built for testnet and listened there — notes never arrived.
   Propagate MIDEN_NETWORK=${E2E_NETWORK:-testnet} through cross-env.

2. The SDK's classic methods worker resolves its WASM at
   new URL('assets/miden_client_web.wasm', self.location.href) → that
   maps to /assets/assets/miden_client_web.wasm inside the extension.
   Nothing was placing the WASM at that path, so the front-end Worker
   WebAssembly.instantiate() threw LinkError. Add a closeBundle step
   that copies the SDK's own WASM (from node_modules/@miden-sdk/miden-sdk)
   to /assets/miden_client_web.wasm and /assets/assets/miden_client_web.wasm.

   Copy from the SDK's bundled output — NOT from dist/chrome_unpacked/
   static/wasm/. The extension build emits its own WASM for the background
   service worker (with distinct wasm-bindgen hash suffixes), and the
   Worker's glue expects the SDK's original binary. Using the extension's
   rebuilt WASM produces WebAssembly.LinkError on import #180.

All 7 E2E blockchain specs pass on devnet (6.3m total).
WiktorStarczewski added a commit that referenced this pull request Apr 14, 2026
* feat: add E2E blockchain test harness with AI-agent observability

Adds a comprehensive end-to-end test harness for testing the wallet
against live Miden networks (testnet/devnet/localhost). Designed for
use as AI agent verification loops with structured failure reports.

Harness includes:
- Two-wallet Playwright fixture (independent Chrome instances)
- miden-client CLI wrapper with auto-install from crates.io
- Observability layer: NDJSON timeline, state snapshots, CLI capture,
  browser console/network logs, failure reports with diagnostic hints
- Agentic debug mode: browsers stay open on failure for hot-reload
- 6 test specs: wallet lifecycle, minting, public/private send,
  multi-claim, multi-account

Source modifications:
- DEFAULT_NETWORK configurable via MIDEN_DEFAULT_NETWORK env var
- Zustand store + intercom exposed behind MIDEN_E2E_TEST flag
- DISABLE_ESLINT webpack guard for E2E builds
- Local miden-client SDK linked via file: protocol

Known issue: webpack's asyncWebAssembly + importScripts deadlocks
the service worker in Playwright's Chrome for Testing. Vite migration
planned to resolve this.

* wip: vite background SW build (phase 1 in progress)

Vite builds the background service worker as ESM with inlineDynamicImports.
The build succeeds and produces a single background.js (2.4MB) with the
WASM SDK inlined. Manifest updated to type: module.

Key files:
- vite.background.config.ts: Vite config with wasm plugin, SVG stubs,
  node polyfills, and SW-safe preload helper patches
- src/background-entry.ts: ESM entry that registers MV3 listeners before
  importing the WASM-heavy background module
- webpack backgroundConfig removed from exports

Current blocker: Chrome extension SWs don't support dynamic import(),
and the WASM SDK's top-level await blocks module evaluation, preventing
the intercom handler from registering until WASM compilation completes.
The intercom timeout on the frontend side causes a blank page.

Next step: restructure so intercom registers before WASM TLA resolves.

* wip: vite background build with early intercom handler

Key progress:
- Vite 8 builds background.js as single ESM file (2.4MB, ~1s build)
- SVG stub plugin, node polyfills, WASM plugin all working
- sw-patches plugin: strips document/window refs, injects early
  intercom handler as banner at top of output
- Early handler responds to GET_STATE_REQUEST and SYNC_REQUEST
  using correct INTERCOM_REQUEST/INTERCOM_RESPONSE message types

Root cause found: Chrome extension MV3 service workers do NOT allow:
- ESM modules with top-level await (SW won't register)
- Dynamic import() (spec restriction on ServiceWorkerGlobalScope)
- Stripping TLA makes SW register but breaks module init order

Next: need to make init_miden_client() (WASM loader) non-blocking
while keeping other module initializers synchronous. The Rolldown
__esmMin pattern used by Vite 8 wraps every module in a lazy init
function with TLA, all of which must be stripped for SW registration.

* feat: vite background SW build resolves Playwright WASM deadlock

The background service worker now builds with Vite 8 as a single ESM
file (2.4MB, ~1.2s build) instead of webpack. This resolves the WASM
chunk loading deadlock that prevented E2E testing in Playwright.

Key changes:
- vite.background.config.ts: Vite config with inlineDynamicImports,
  SVG stubs, node polyfills, WASM plugin, and sw-patches plugin that
  strips TLA and patches document/window refs for SW compatibility
- background-entry.ts: ESM entry point (MV3 listeners in background.ts)
- manifest.json: service_worker=background.js with type=module
- main.ts: frontStore moved inside start() (deferred init for TLA compat)
- actions.ts: getFrontState returns Idle state immediately when store
  not yet initialized (prevents infinite recursion during WASM TLA)
- IntercomServer: queues messages when no handler registered, replays
  when handler is added; responds to GetState/Sync early

The wallet now loads in Playwright in ~2s (SW registers in 1s, welcome
screen visible in 2s).

* wip: full Vite extension build (UI pages + background)

Extension now builds entirely with Vite 8 (~3.7s total):
- vite.extension.config.ts: UI pages with code splitting, custom SVG
  transform, public asset copy, manifest transform
- vite.background.config.ts: Background SW as inlined ESM
- HTML entry points moved to project root with <script type="module">
- No webpack dependency for extension build

Current blocker: runtime error "Cannot use 'in' operator to search for
'animation' in undefined" -- a CSS animation support check in a
dependency gets undefined instead of document.body.style. Need to
investigate which dependency and add proper polyfill/guard.

* feat: complete Vite migration for Chrome extension (no webpack)

Full extension builds with Vite 8 in ~3.7s (was ~25s with webpack):
- Background SW: 1.1s (inlined ESM, TLA stripped, SW-safe patches)
- UI pages: 2.6s (code-split, SVG transform, React global hoist)

Key fixes for Vite 8 / Rolldown compatibility:
- Custom SVG→React transform plugin (vite-plugin-svgr incompatible)
- React global hoisting for CJS deps using React.createElement
- process global injection via HTML transform
- nodePolyfills only for background (breaks document.createElement in UI)
- crossorigin attribute removal from extension script tags
- HTML entry points at project root (public/ not processed by Vite)

Build: rimraf dist && vite build --config vite.background.config.ts
       && vite build --config vite.extension.config.ts

* fix: add @tailwindcss/vite plugin for proper CSS generation

* feat: complete Vite migration for all build targets

All builds now use Vite 8:
- Extension: vite.background.config.ts + vite.extension.config.ts (~3.7s)
- Mobile: vite.mobile.config.ts (~2.7s)
- Desktop: vite.desktop.config.ts (~2.7s)
- Total: ~9s for all targets (was ~50s+ with webpack)

No webpack configs are used for any build target.

* chore: remove webpack entirely

Removed all webpack configs, dependencies, and related files:
- webpack.config.js, webpack.html.config.js, webpack.public.config.js
- webpack.mobile.config.js, webpack.desktop.config.js
- .swcrc (Vite uses @vitejs/plugin-react-swc)
- public/sw.js (background.js is the SW with type:module)
- 16 webpack dependencies removed from package.json

All builds now use Vite 8 exclusively:
- Chrome extension: ~5.4s (bg 1.8s + ui 3.6s)
- Mobile: ~2.3s
- Desktop: ~2.1s
- Total: ~9.7s (was ~50s+ with webpack)

* fix: CSP-safe globals injection + fixture locator fixes

- Move process/global injection from inline script to external globals.js
  (inline scripts blocked by extension CSP)
- Fix fixture locator: use .or() instead of CSS comma selector with
  text= pseudo-selector (Playwright parses the whole thing as CSS)
- Add retry loop for "Create a new wallet" click -- WASM SDK may not
  be ready when the button is first clicked

Remaining issue: the Miden WASM SDK's internal initialization
(WebClient creation, WASM instance setup) never completes in the SW
context of Playwright's Chrome for Testing. The UI renders and the
intercom responds, but wallet operations that need the SDK hang.
This is a limitation of the WASM SDK running in a service worker
without Web Workers support.

* fix: resolve WASM SDK init in Vite SW build

Three critical fixes for the Vite-built service worker:

1. __vitePreload passthrough: With TLA stripped, the preload helper's
   lazy init runs fire-and-forget, leaving __vitePreload undefined when
   loadWasm() calls it. Inject a passthrough at the top of the file.

2. Re-await critical init functions inside start(): The stripped TLA
   causes module-scoped variables (Vault, store, etc.) to be undefined.
   Collect init_* calls and re-inject them inside start() after the
   intercom handler registration.

3. Buffer polyfill: Add explicit Buffer import to all entry points
   (background-entry.ts and UI pages) since nodePolyfills shim runs
   fire-and-forget with TLA stripping.

Result: Welcome screen in 0.1s, wallet creation works after ~15s
(WASM init completes in background).

* fix: re-inject ALL init functions including init_miden_client

The init_fetchBalances → init_store → init_activity → init_dapp chain
transitively awaits init_miden_client(). Excluding it from re-injection
caused the entire chain to hang because the fire-and-forget Promise
hadn't resolved yet.

With all inits re-injected (including WASM), the total init time is
~100ms (WASM compiles in 30ms). The wallet creation flow now works
end-to-end in Playwright.

Also fixed regex to capture init names with $ (e.g., init_helpers$2,
init_store$1) using [\w$]+ instead of \w+.

* fix: forced StateUpdated broadcast after start() completes

Add explicit StateUpdated broadcast at end of start() to ensure the
frontend re-fetches state after all module inits complete. Without
this, wallet operations that complete before frontStore.watch() was
set up wouldn't trigger UI updates.

Current state: onboarding flow works (welcome → seed → verify →
password → ready → get started), but after wallet creation the app
returns to welcome screen instead of Explore. Investigating whether
registerNewWallet actually succeeds in the SW context.

* wip: E2E onboarding flow reaches seed phrase + verify + password + ready

The wallet onboarding flow works up to "Your wallet is ready" screen:
- SW registers in 1s, welcome screen in 0.1s
- Seed phrase backup + verification works
- Password creation works
- Confirmation screen shows "Your wallet is ready"

Remaining issue: registerNewWallet (which calls Vault.spawn → WASM SDK)
appears to fail silently in the SW context. After clicking "Get Started",
the page returns to the welcome screen instead of the Explore page.
The backend's GetStateRequest returns status=Idle even after the
onboarding "completes", suggesting the wallet creation failed.

Next steps:
1. Add error tracking to registerNewWallet / Vault.spawn to find the
   exact failure point
2. The WASM SDK compiles and instantiates correctly (verified), but
   higher-level operations (account creation, HD key derivation) may
   use APIs not available in service workers
3. Alternative: test if the issue also exists in Brave (where the
   wallet works normally)

* wip: Vite SW init tracking - found architecture issue

Root cause identified: the backend's init_actions() transitively
imports frontend modules (init_front, init_provider, init_client,
init_balance) through the init_dapp → init_activity → init_store
(Zustand) → init_fetchBalances chain. These frontend modules
reference React, DOM APIs, or browser-only features that can't
initialize in a service worker.

This is a wallet architecture issue: the backend code (dapp.ts,
actions.ts) imports from the frontend module graph. With Rolldown's
__esmMin lazy init pattern, ALL transitive dependencies are
initialized when await init_actions() is called.

The WASM SDK itself initializes successfully (30ms compile,
verified). The hang is in the frontend modules.

Options:
1. Refactor the wallet to separate backend/frontend imports
2. Stub out frontend modules for the SW build
3. Use dynamic import() for frontend deps in backend code
   (but import() is banned in SWs)

* fix: module-scope __initsReady + handlePreloadError swallow

- Move __initsReady Promise to module scope (was stuck inside start())
- Include await init() (Actions.init) in __initsReady
- Exclude init_actions and init_transaction_processor (they pull in
  frontend modules that can't init in SW)
- processRequest awaits __initsReady for non-GetState/Sync requests
- Prevent handlePreloadError from throwing (log instead)

Verified: __initsReady resolves in 47ms, handleMessage receives
NewWalletRequest correctly, processMessage + processRequest both
fire. The intercom pipeline works end-to-end.

* fix: E2E wallet creation passes on devnet

- Add util polyfill to both Vite configs (fixes vault crypto libraries
  needing util.debuglog)
- Set publicDir: false in extension config to prevent Vite from
  overwriting processed HTML with raw copies from public/
- Add miden.io to manifest host_permissions (for RPC endpoints)
- Improve seed word extraction in wallet-page.ts (more robust DOM
  traversal)
- Add detailed Vault.spawn step logging for debugging
- Add intercom server logging for non-trivial messages
- Skip lock/unlock test (init_actions excluded from __initsReady
  leaves unlockQueue uninitialized - needs architectural fix)
- Support devnet address prefix (mdev) in test assertions

* fix: lazy PQueue init + lock/unlock + CLI sync on init

- Make dappQueue and unlockQueue lazy-initialized on first use instead
  of at module scope. In the Vite SW build, init_actions can't fully
  complete because it transitively imports dapp.ts which hangs on
  frontend module stubs. Lazy init ensures the queues work regardless.
- Re-enable lock/unlock test (now passes with lazy queues)
- Add sync after miden-client init (genesis block required for account
  creation)

* fix: balance reads consumable notes + trigger transaction processing

- getBalance now reads from both vault assets (consumed) and
  miden_sync_data.notes (consumable/pending) from chrome.storage.local
- triggerSync also sends PROCESS_TRANSACTIONS_REQUEST to kick off
  auto-consume of pending notes
- This fixes balance detection when the SW transaction processor
  can't auto-consume due to init_activity not completing

* fix: two-phase SW init + lazy transaction processor + send flow selectors

- Restructure __initsReady into core (must-complete) and extended
  (fire-and-forget with 30s timeout) phases. Core inits complete
  before processRequest runs; extended inits (init_actions,
  init_transaction_processor) run in background.
- Add lazy wait for safeGenerateTransactionsLoop in transaction
  processor - Rolldown's re-export of transactions.ts doesn't await
  the async module init, so the function may not be ready immediately.
- Fix send flow selectors: CardItem is a <div> not <article>;
  address input is <input> not <textarea>.

* fix: break circular init deadlock + handle duplicate seed words

- Break init_store → init_fetchBalances circular await deadlock by
  making init_store not await init_fetchBalances (it only defines
  runtime functions, not needed at store creation time)
- Import transaction functions directly from activity/transactions
  instead of through activity/index.ts re-export
- Add lazy wait loop in transaction processor for safeGenerateTransactionsLoop
- Handle duplicate words in seed phrase verification (click 2nd instance)

* fix: inject onInstalled listener into background.js, bump to 1.14.1

* fix: window→globalThis for E2E hooks + init_store deadlock resolved

The root cause of init_transactions hanging was window.__TEST_STORE__
at the end of init_store - window is undefined in service workers.
Changed to globalThis which works in both page and SW contexts.

Also aligned MIDEN_NETWORK env var with main branch convention.

* fix: window→globalThis unblocks init_transactions + claim attempts

- The root cause of init_store hanging was window.__TEST_STORE__ at the
  end of init_store - window is undefined in service workers. Changed
  to globalThis. This fully unblocks init_transactions, making
  safeGenerateTransactionsLoop available.
- Added claimAllNotes method to wallet-page.ts (not yet working:
  auto-consume only triggers for Miden faucet notes, not custom
  test faucet notes. Need to click Claim buttons in UI.)
- Added claim_notes step to send-private and send-public tests.

* fix: claim notes via UI - reload page for fresh Dexie + inject metadata

- Reload page before claiming to fix DatabaseClosedError (clearStorage
  during wallet creation closes the frontend's Dexie handle)
- Inject metadata into Zustand store for custom faucet tokens so they
  appear in useExtensionClaimableNotes (filtered by metadata existence)
- Navigate to /receive and click Claim All button
- Vault balance reaches 1000 after claim (note consumption works!)

* wip: mobile Vite build - WASM loads but React doesn't mount (needs investigation)

* wip: mobile Vite build - splash loads, blocked on module worker

Root cause: the SDK's Web Worker uses import() to load the WASM glue,
which requires { type: "module" } workers. WKWebView (iOS/Safari) does
NOT support module workers — only Chromium does.

What works:
- async IIFE wrapper makes TLA legal in classic scripts
- publicDir: true provides misc/ assets
- import.meta.url replaced with document.currentScript.src
- inlineDynamicImports bundles everything into one file

What's needed (SDK-level fix):
- The worker file (web-client-methods-worker.js) must be self-contained
  with no import() calls, OR the SDK must support a main-thread fallback
  that actually initializes the WASM client (current no-Worker path just
  sets wasmWebClient=null).

* wip: mobile Vite build - root cause found

The SDK's WASM glue code uses top-level await (TLA) in the worker's
Cargo-*.js file. This means:
- Workers must be ESM ({ type: "module" }) to support TLA
- WKWebView (iOS Safari) does NOT support module workers
- Worker format: 'iife' fails because TLA is illegal in IIFE

Webpack's build worked because its async module system (webpack runtime)
wraps TLA in its own async handler within classic scripts.

Fix needed in miden-client SDK: wrap WASM glue TLA in an async init
function so the worker can be bundled as a classic script.

* wip: mobile - worker now classic script, WASM double-init blocks main thread

Progress:
- SDK worker wrapped in async IIFE (no module worker needed)
- import.meta replaced, exports stripped, dynamic imports inlined
- Worker creates as classic worker (no type:"module")
- WASM copied to unhashed path for worker access

Remaining issue: main bundle's init_Cargo_DzVuXZk9() compiles WASM on
the main thread (14MB, blocks async IIFE). Webpack avoided this because
it only loaded WASM in the Worker, not the main thread. Need to either:
1. Strip the main-thread WASM init from the bundle
2. Or defer it so it doesn't block the IIFE

* wip: mobile - full TLA strip works (no hang), needs __initsReady

With all TLAs stripped, the module evaluates instantly (no hang).
But IconName.Coins is undefined because init_v2 hasn't completed.
Need __initsReady pattern (same as SW build) to await safe inits
before the app entry function renders React.

* wip: mobile - WASM init skipped, module runs past Cargo but hangs later

Skipping __wbg_init on main thread works (console.log fires).
The module continues evaluation but hangs at a later TLA.
Need to identify which TLA after line 40177 blocks.
The approach is correct - just need to find and skip/defer more blocking awaits.

* wip: mobile Vite build - needs architectural fix for WASM TLA in WKWebView

* wip: mobile Vite build - sync factory approach (in progress)

Attempted approach: convert __esmMin async factories to sync execution
by stripping await from init_*() calls, skipping WASM init/finalize,
and wrapping in async IIFE. Gets close but Vite's build-import-analysis
re-parses the output and fails on the modified code.

Key findings:
- type="module" + inlineDynamicImports = WKWebView silently fails to evaluate
- defer + async IIFE + TLA strip = IconName undefined (inits fire-and-forget)
- defer + async IIFE + TLA strip + __initsReady = build fails (worker re-processing)
- Sync factory approach = promising but Vite re-parses output and chokes

The root blocker is Vite's worker processing: it detects new Worker(new URL(...))
and re-bundles the worker file, which has TLA that can't be IIFE format.
Even @vite-ignore doesn't prevent this in Vite 8/Rolldown.

Next step: either disable Vite's worker detection entirely via a resolveId
hook, or do the sync factory transform BEFORE Rolldown bundles (in a
transform hook instead of generateBundle).

* fix: mobile Vite build loads with SDK TLA removed

Removing await __wbg_init() from the SDK Cargo glue eliminates the
module-scope TLA that blocked WKWebView module evaluation. The module
now loads and evaluates successfully (confirmed with test HTML).

The app shows beige background (HTML renders) but React doesn't mount.
initMobile() hangs — likely the mobile adapter init or Worker creation.
This is a runtime issue, not a module-loading issue.

Config: code-splitting enabled, type="module", no hacks needed.

* fix: mobile Vite build renders on iOS!

Two fixes:
1. SDK: Remove await __wbg_init() TLA from Cargo glue (done in
   node_modules for now, needs permanent SDK change). This eliminates
   the module-scope TLA that blocked WKWebView ESM evaluation.

2. Wallet: Hoist React to globalThis in mobile-app.tsx entry point.
   CJS libraries (react-day-picker, etc.) reference bare React.createElement
   without importing it. Vite's CJS interop scopes React to a local var
   but these libraries expect a global.

Result: Welcome/onboarding screen renders on iOS simulator.
The WASM client init error is expected (main-thread WASM not loaded) —
the Worker handles all WASM operations.

* chore: remove mobile debug overlay and error scripts

* feat: add onboarding bypass for mobile testing + CDP docs + fix WASM path

- Add ?__test_skip_onboarding=1 URL param to skip onboarding and jump
  to "Your wallet is ready" screen with auto-generated seed + password
- Fix WASM copy in closeBundle hook: look in static/ not assets/
- Add CDP bridge bringup recipe and onboarding bypass docs to CLAUDE.md

* feat: add Miden Vite plugin + inspect CLI CDP patch

- Add @miden-sdk/vite-plugin to mobile config for WASM dedup and COOP/COEP headers
- Drop custom worker.format: 'es' (handled by the plugin now)
- Save patches/inspect-cli-cdp-fix.patch that fixes two bugs in
  @inspectdotdev/cli v2.1.1: URL-encoded pipe in target IDs and race
  condition between unselectTarget and activeTargetId cleanup

* chore: add :devnet script variants for mobile builds

Adds build:mobile:devnet, mobile:sync:devnet, mobile:ios[:build|:run]:devnet,
and mobile:android:devnet that set MIDEN_NETWORK=devnet explicitly.

The MIDEN_NETWORK env var is baked into the bundle at build time. Without
:devnet variants, users have to remember 'MIDEN_NETWORK=devnet yarn ...'
every time; forgetting produces a testnet build that silently fails on
mobile because testnet's gRPC-web proxy returns the wrong content-type.

* docs: drop point-in-time testnet/devnet explanation from CLAUDE.md

* fix(mobile): add !important to body safe-area padding

Tailwind 4 preflight + app CSS loads after mobile.html's inline <style>
and was zeroing out the env(safe-area-inset-*) padding, causing content
to render under the iPhone status bar. !important forces the inline
safe-area rules to win the cascade.

* fix(mobile): emit SVG file URL as default export

The svg-to-react Vite plugin was exporting the empty string as default,
so `import Logo from '*.svg'; <img src={Logo}>` rendered no image
(and browsers fell back to the alt text, e.g. 'Miden Wallet Logo').

Now mirror @svgr/webpack: default export is a Vite-hashed asset URL,
named export ReactComponent is still the JSX component. Both patterns
are used across the wallet codebase.

* fix(mobile): restore periodic sync loop removed in v14 migration

The v14 migration (#151) deleted the AutoSync class and replaced it with
useSyncTrigger, but useSyncTrigger only runs on extension — it early-returns
for all other platforms. Mobile and desktop stopped syncing entirely: after
the initial Vault.spawn sync during wallet creation, nothing ever called
client.syncState() again, so notes never arrived and balances never updated.

useSyncTrigger now fires on mobile/desktop too, calling client.syncState()
directly under the wasm client lock every 3s — mirroring exactly what the
old AutoSync.sync() loop did. Same guards as before: skip while on the
generating-transaction page or while the mobile tx modal is open, to avoid
queuing sync behind a long proof.

* fix(mobile): flip isSyncing around each syncState call

The header spinner watches hasCompletedInitialSync, which only flips to
true when setSyncStatus(false) fires. On extension that happens when the
SW broadcasts SyncCompleted; on mobile/desktop the direct-call path
skipped this entirely, so the spinner stayed up forever even though
sync was actually running.

Wrap each iteration in setSyncStatus(true) / setSyncStatus(false),
mirroring the old AutoSync pattern.

* feat(settings): 3-state theme picker (system/light/dark), default system

- ThemeSetting = 'light' | 'dark' | 'system' (was 'light' | 'dark');
  DEFAULT_THEME is now 'system'
- resolveTheme() consults window.matchMedia('(prefers-color-scheme: dark)')
  when the user picks 'system'
- initTheme() attaches a single media-query listener that re-applies the
  theme whenever the OS flips light/dark, but only while the user's
  setting is 'system' (explicit picks are not overridden)
- Replace the Dark Mode on/off toggle in General Settings with a TabPicker
  segmented control (System / Light / Dark)
- Keep toggleTheme() as a deprecated shim for any lingering callers

* fix(theme): dark-mode polish across navbar, Theme picker, Settings labels

- Native navbar (iOS): NavbarButton/NavbarSecondaryButton inactiveColor is
  now a dynamic UIColor — dark grey in light mode (unchanged), white in
  dark mode so the label stays legible against .systemUltraThinMaterial.
- Theme row in General Settings: label uses the same font-size / line-height
  as SettingToggle titles (text-base, leading-[130%]) and picks up
  dark:text-white so it's visible in dark mode.
- SettingToggle: dark:text-white on the title so other settings labels
  follow suit.
- TabPicker: dark variants for container (surface-secondary), active pill
  (white/15 — mirrors light mode's white-on-grey contrast), text labels
  (dark:text-white), and icon fill (resolved from document.documentElement
  at render time since SVG fill can't use CSS dark: variants).

* fix(theme): drop counterproductive dark: variants and use theme tokens

The Tailwind config already maps 'black'/'white' to CSS variables that
flip with .dark (text-black → --color-text-primary = black/white,
bg-white → --color-surface = white/translucent-dark). Adding explicit
dark:text-white actually OVERRIDES the good default with --color-surface
(dark translucent grey) in dark mode — making labels invisible.

- Remove dark:text-white everywhere I'd added it (SettingToggle,
  GeneralSettings, TabPicker).
- TabPicker container: bg-grey-50 (fixed #F3F3F3) → bg-gray-50
  (--color-surface-tertiary: same #f3f3f3 in light, #333333 in dark).
- TabPicker active pill: bg-white is already theme-aware but goes too
  subtle in dark, so ADD dark:bg-pure-white/15 to keep proper contrast.
- Icon fill: keep JS-time isDarkMode lookup (SVG fill can't use dark:).

* fix(theme): Secondary button readable in dark mode; document Tailwind tokens

- Secondary button bg was literal bg-[#E9E4E4] (not theme-aware), while
  text-heading-gray flips to white in dark mode → white text on light
  beige was barely visible (most obvious on the Reveal Seed Phrase
  'Close' button). Add dark:bg-gray-50 (→ #333333) + matching hover.
- CLAUDE.md: document which Tailwind color tokens auto-flip via CSS
  variables vs which are fixed literals, and when explicit dark:
  variants help vs hurt. Quick-reference so I stop layering dark:
  variants on top of already-theme-aware tokens.

* fix(theme): explicit text-black on FormField <input> so dark mode inverts

The <input> had no text color class, so it was picking up the browser
default (color: black). On the dark-mode field background that made the
password mask dots invisible. Adding text-black opts into the theme-aware
--color-text-primary variable (black in light, white in dark).

* fix(e2e): build extension for the right network + copy SDK WASM to worker path

Two regressions in the E2E blockchain test pipeline, both surfaced when
switching to devnet:

1. test:e2e:blockchain:build was hardcoded to testnet. MIDEN_NETWORK was
   never propagated to the extension build, so with E2E_NETWORK=devnet
   the test harness deployed faucets on devnet while the wallet quietly
   built for testnet and listened there — notes never arrived.
   Propagate MIDEN_NETWORK=${E2E_NETWORK:-testnet} through cross-env.

2. The SDK's classic methods worker resolves its WASM at
   new URL('assets/miden_client_web.wasm', self.location.href) → that
   maps to /assets/assets/miden_client_web.wasm inside the extension.
   Nothing was placing the WASM at that path, so the front-end Worker
   WebAssembly.instantiate() threw LinkError. Add a closeBundle step
   that copies the SDK's own WASM (from node_modules/@miden-sdk/miden-sdk)
   to /assets/miden_client_web.wasm and /assets/assets/miden_client_web.wasm.

   Copy from the SDK's bundled output — NOT from dist/chrome_unpacked/
   static/wasm/. The extension build emits its own WASM for the background
   service worker (with distinct wasm-bindgen hash suffixes), and the
   Worker's glue expects the SDK's original binary. Using the extension's
   rebuilt WASM produces WebAssembly.LinkError on import #180.

All 7 E2E blockchain specs pass on devnet (6.3m total).

* feat(e2e): yarn test:e2e:blockchain:{testnet,devnet,localhost} shortcuts

Before: 'E2E_NETWORK=devnet yarn test:e2e:blockchain' worked but was easy
to forget, and a mismatch between E2E_NETWORK (harness) and the wallet's
baked-in MIDEN_NETWORK silently failed with timeouts. CLAUDE.md also
referred to a non-existent MIDEN_DEFAULT_NETWORK env var.

Add three explicit :network variants — each sets E2E_NETWORK via
cross-env, which then propagates through to the build via the existing
${E2E_NETWORK:-testnet} fallback. Update CLAUDE.md to recommend them.

* docs(readme): E2E blockchain test commands + per-network shortcuts

* refactor(e2e): WalletPage interface + ChromeWalletPage class, rename page→target

Prep for the iOS E2E port (see plan at ~/.claude/plans/shimmering-enchanting-ember.md):

- helpers/wallet-page.ts: extract WalletPage interface from the existing
  public signatures verbatim (no shape changes). Rename the concrete class
  to ChromeWalletPage. Add ChromeWalletPageApi extends WalletPage with the
  Chrome-specific page/extensionId/userDataDir that the fixture + a handful
  of specs reach into directly.

- fixtures/two-wallets.ts: TwoWalletFixtures.{walletA,walletB} retyped
  WalletPage → ChromeWalletPageApi, constructor call updated.

- harness/types.ts + harness/test-step.ts: rename the options-array field
  from 'page' → 'target' on screenshotWallets and captureStateFrom. Chrome
  side keeps passing walletA.page (Playwright Page); the iOS side will pass
  walletA directly (IosWalletPage with matching screenshot/evaluate shape).

- 5 specs (mint-and-balance, multi-account, multi-claim, send-public,
  send-private): 'page: walletA.page' → 'target: walletA.page'.

Zero runtime behavior change. Chrome E2E suite still 7/7 on devnet.

* fix(e2e): seed-phrase verify clicks wrong word on prefix collision

Playwright `button:has-text("fold")` is a substring match — it also
matches `unfold`. The verify screen checks shuffledWords[idx] by index,
so .first() picking the wrong button left Continue disabled.

Read the article's button texts and click by exact-match index instead.

* refactor(e2e): platform-neutral SnapshotCaps for harness

Replace Page+BrowserContext+extensionId in captureWalletSnapshot with a
SnapshotCaps record of pre-bound closures (readStore, hasIntercom,
serviceWorkerStatus, currentUrl). The Chrome fixture builds these from
its Page+context once at setup; test-step looks them up by wallet label
and stays runtime-agnostic.

Also: rename screenshot/state-capture target types from Playwright Page
to ScreenshotCapable/StateCaptureCapable structural types; add ios/chrome
discriminator + optional extensionId+serviceWorkerStatus on
WalletSnapshot; rename RunManifest.chromeVersion to runtimeInfo;
add 'app_crash' to FailureCategory.

Prerequisite for the iOS test harness — IosWalletPage will satisfy the
same capability surfaces with no harness changes.

* feat(e2e): iOS Simulator test harness — full 7-spec parity

Adds a parallel iOS suite that exercises the same flows as the Chrome E2E
suite against two iPhone 17 / iPhone 17 Pro simulators in parallel.

Architecture:
  - SimulatorControl: thin xcrun simctl wrapper. reservePair() persists
    UDIDs at test-results-ios/.device-pair.json so successive runs reuse
    the same booted devices.
  - CdpBridge: per-simulator WebKit Inspector connection via
    appium-remote-debugger over the webinspectord_sim UNIX socket. Two
    parallel sessions, one per device.
  - IosWalletPage: WalletPage interface implementation backed by CdpSession
    + SimulatorControl. Skips the seed-phrase UI via __TEST_SKIP_ONBOARDING.
  - two-simulators fixture: same shape as two-wallets so specs port with
    a one-line import change.
  - All 7 specs ported as *.ios.spec.ts; the 5 simple ones changed only
    the import path + screenshot/state target field; multi-account uses
    the iOS POM helpers (delay, locatorText) where Chrome reaches into
    walletA.page.

Build:
  - playwright.ios.config.ts: 10-min per-test timeout (cold WASM compile),
    separate test-results-ios output, global setup that asserts App.app
    exists and reserves+boots the device pair.
  - yarn test:e2e:mobile:{build,run,testnet,devnet}.

Empirical run + flake check still pending.

* docs: iOS Simulator E2E test harness section in CLAUDE.md

* fix(e2e/ios): bring-up fixes — CDP eval semantics, evalAsync timeout, MIDEN_E2E_TEST plumbing

After end-to-end testing on devnet, made the iOS harness wallet-lifecycle
passing (both tests, ~42s wall clock):

CDP layer (cdp-bridge.ts):
  - eval no longer wraps body in 'return (...)' — that breaks
    multi-statement bodies. Callers include their own 'return' statement.
  - Added evalAsync for promise-returning code (execute_async_script atom)
    with a 30s outer timeout to fail fast when scripts never invoke their
    callback.
  - selectApp now polls up to 60s for the WebView to register with
    webinspectord_sim — 3s after launch is too short on cold boot.

IosWalletPage:
  - createNewWallet uses the wallet's official URL bypass
    (?__test_skip_onboarding=1&password=...) — Welcome.tsx already supports
    this; the hand-rolled __TEST_ONBOARDING_PASSWORD global I made up
    didn't exist.
  - lockWallet fires LOCK_REQUEST without awaiting the intercom roundtrip
    (which can hang on mobile when the SW-style port resolves on the same
    thread); reload immediately and let the lock take effect.
  - getBalance / claimAllNotes balance-check read the Zustand store
    directly instead of awaiting fetchBalances — that path can deadlock
    behind useSyncTrigger's WASM client lock.
  - triggerSync simplified to a sleep — useSyncTrigger auto-syncs every
    3s on mobile (no SW indirection), so no SYNC_REQUEST is needed.
  - pollForSelector tolerates eval errors mid-poll (page reloads).

Vite mobile build:
  - vite.mobile.config.ts now plumbs MIDEN_E2E_TEST through to the bundle.
    Without this, __TEST_STORE__ / __TEST_INTERCOM__ are never installed
    on mobile, so the harness can't see wallet state.

Docs:
  - CLAUDE.md "Empirical Status" subsection — wallet-lifecycle ✅,
    sync-dependent specs blocked on mobile-side auto-process gap (see
    notes in commit body for resolution paths).

* docs(e2e/ios): correct the mobile auto-process story

Earlier commit claimed 'mobile has no auto-process' — that was wrong.

Both Chrome and mobile auto-consume identically, in Explore.tsx's
autoConsumeMidenNotes → startBackgroundTransactionProcessing. The gate
is the same on both: only notes whose faucetId === midenFaucetId (the
well-known MIDEN token) get auto-consumed. Custom faucets (which E2E
tests use) require manual claim on both platforms.

The asymmetry Chrome's test relies on is purely in getBalance: Chrome's
reads chrome.storage.local.miden_sync_data.notes and counts pending
notes as part of the displayed balance, so Chrome tests pass without a
claim step. Mobile has no chrome.storage — the test-side patch (expose
getConsumableNotes via a hook) was attempted but is incompatible with
the WASM client's single-access invariant once useSyncTrigger is also
holding the lock.

So: iOS balance-after-mint specs need an explicit
await walletX.claimAllNotes() before waitForBalanceAbove. This is
actually the honest user flow on mobile. Add a clarifying comment to
IosWalletPage.getBalance and a corrected 'Empirical Status' block in
the CLAUDE.md iOS section.

* feat(e2e/ios): full 7/7 spec parity on devnet

After end-to-end bring-up, all 7 iOS specs pass in ~9 min wall clock
(devnet). Changes required to get there:

harness:
  - New IosWalletPage.triggerNavbarAction: the wallet's primary CTAs
    (Claim All, Continue-in-Send, Confirm-in-Send) are hoisted to a
    native iOS navbar overlay — a separate UIWindow outside the WebView
    that CDP can't see and xcrun simctl can't tap. Trigger via a small
    JS hook exposed only in mobile E2E builds.
  - claimAllNotes drops the location.reload() Chrome uses; on mobile
    that drops the in-memory vault key and bounces to the password
    screen (no SW to hold the unlock). Stay in-session.
  - sendTokens uses triggerNavbarAction for both Continue and Confirm
    (SendDetails + ReviewTransaction both register native actions).
  - Playwright test timeout raised to 15 min for claim-heavy specs
    (multi-claim consumes 3 notes, each ~60-90s WASM prove on sim).

specs (.ios.spec.ts):
  - mint-and-balance, multi-account, multi-claim, send-public,
    send-private: claim explicitly before waitForBalanceAbove. Chrome
    gets away without a claim because its getBalance reads
    chrome.storage.local.miden_sync_data.notes and counts pending notes
    as balance; mobile has no chrome.storage, so claim is the honest
    path. (Both platforms auto-consume only MIDEN-faucet notes; custom
    faucets have always needed manual claim on both.)

wallet product:
  - src/lib/dapp-browser/use-native-navbar-action.ts: one ~10-line E2E
    test hook that exposes __TEST_TRIGGER_NAVBAR_ACTION__() to JS.
    Gated on MIDEN_E2E_TEST=true AND isMobile() so the Chrome bundle
    is byte-identical to pre-iOS. This is the only product-code change
    the iOS harness needed.

docs: empirical status block in CLAUDE.md now lists passing counts,
timings, and all the gotchas the port surfaced.

* chore: switch @miden-sdk/{miden-sdk,react} from local file: links to published 0.14.1

* chore: update translation files

* chore: add test:e2e:mobile:localhost shortcut for parity with chrome e2e

* docs: README section for iOS Simulator E2E tests

* fix(ci): resolve lint errors and unit test failures

- Auto-format with prettier across touched files.
- Add /* eslint-disable import/first, import/order */ to entry-point .tsx
  files (popup, options, sidepanel, fullpage, confirm, mobile-app,
  background-entry) where the Buffer/React polyfill must run before
  imports — the polyfill order is intentional per commit 438d8bd and
  this is the right escape hatch for it.
- Remove dead __RNW_* debug timing markers from registerNewWallet that
  used 'self' (no-restricted-globals); they were never read anywhere.
- Fix i18n/loading.ts dynamic import to handle CJS-style default export
  (uses .default ?? mod, matching storage-adapter and intercom/client).
- Update intercom server.test.ts: the old 'returns undefined when no
  handlers' test asserted behavior that changed — non-GET_STATE/SYNC
  messages now QUEUE for replay instead of getting an immediate
  undefined response. New test asserts the queue-and-replay flow.
- Update actions.test.ts: getFrontState no longer retries when
  inited=false; it returns Idle immediately so the UI can render while
  the backend boots. Test updated to reflect the new immediate-Idle
  contract.
- Fix transaction-processor.test.ts mock path: the SUT imports from
  lib/miden/activity/transactions directly (to avoid the activity/
  re-export's circular init deadlock in the Vite SW bundle), so the
  test mock has to target the same path; the previous mock at
  lib/miden/activity didn't intercept.
- Drop unused locals (tokenSymbol param in Chrome getBalance,
  clickInTestId helper in iOS POM, unused TypeScript imports).
- Fix GeneralSettings handleThemeTabChange to guard against
  themeOptions[index] being undefined under strict TS array-access.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no changelog Skip changelog check for trivial changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant